/**
 * Classe utilitária para realizar tarefas via reflexão em objetos.
 * 
 * @author Vitor Franco do Carmo(vitor.carmo@capgemini.com) 07/10/2011
 */
package br.com.vfcfw.commons.util;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.inject.Qualifier;
import javax.persistence.Id;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.log4j.Logger;
import org.hibernate.LazyInitializationException;

import br.com.vfcfw.commons.domain.entity.BaseEntity;
import br.com.vfcfw.commons.exception.ApplicationException;

/**
 * Classe utilitária para realizar tarefas via reflexão em objetos.
 * 
 * @author Vitor Franco do Carmo(vitor.carmo@capgemini.com) 07/10/2011
 */
public class ReflexaoUtil {

    private static final Logger LOGGER = Logger.getLogger(ReflexaoUtil.class.getName());

    private static final String SET = "set";

    private static final String GET = "get";

    private static final String IS = "is";

    private static final String TO = "TO";

    private static final String PO = "PO";

    /**
     * Realiza a cópia dos valores dos atributos do objeto de origem para o objeto de destino (<tt>quando o abributo no
     * objeto de origem tiver o mesmo nome de um atributo no objeto de destino</tt>).
     * 
     * @param origem {@link T} objeto origem.
     * @param destino {@link V} objeto destino.
     * @return Objeto de destino preenchido.
     * @throws IllegalArgumentException
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static <T, V> V copyBean(T origem, V destino) {

        Map<String, Field> mapAtributosObjetoOrigem = gerarMap(origem.getClass().getDeclaredFields());
        Map<String, Field> mapAtributosObjetoDestino = gerarMap(destino.getClass().getDeclaredFields());

        try {

            for (String toFieldName : mapAtributosObjetoOrigem.keySet()) {

                Object obj = null;

                Field atributoOrigem = mapAtributosObjetoOrigem.get(toFieldName);
                Field atributoDestino = mapAtributosObjetoDestino.get(toFieldName);

                // Nao copia atributos staticos
                if (Modifier.isStatic(atributoOrigem.getModifiers())) {
                    continue;
                }

                if (atributoDestino == null) {
                    continue;
                }

                obj = getFromField(origem, atributoOrigem);

                if (obj == null) {
                    setToField(destino, atributoDestino, obj);
                    continue;
                }

                // --------------------------------------------------------------------------------------
                if (atributoOrigem.getType() == atributoDestino.getType()
                    && atributoOrigem.getType() != List.class
                    && atributoOrigem.getType() != Set.class) {
                    setToField(destino, atributoDestino, obj);
                    continue;
                }

                try {

                    if (atributoOrigem.getType() == Set.class && atributoDestino.getType() == List.class) {

                        if (!((Set<?>) obj).isEmpty()) {

                            for (Object element : ((Set<?>) obj)) {

                                if (element.getClass().getPackage().equals(origem.getClass().getPackage())) {

                                    Object objDest = getFromField(destino, atributoDestino);

                                    obj = copyList((Collection<?>) obj, (Collection<T>) objDest,
                                        obterClassDestino(destino.getClass(), element.getClass().getSimpleName()));
                                }

                                break;
                            }
                        } else {
                            obj = new ArrayList();
                        }
                    } else if (atributoOrigem.getType() == List.class && atributoDestino.getType() == Set.class) {

                        if (!((List<?>) obj).isEmpty()) {

                            if (((List<?>) obj).get(0).getClass().getPackage().equals(origem.getClass().getPackage())) {

                                Object objDest = getFromField(destino, atributoDestino);

                                obj = copyList(
                                    (Collection<?>) obj,
                                    (Collection<T>) objDest,
                                    obterClassDestino(destino.getClass(), ((List<?>) obj).get(0).getClass()
                                        .getSimpleName()));
                            }
                        } else {
                            obj = new HashSet();
                        }
                    } else if (atributoOrigem.getType() == List.class && atributoDestino.getType() == List.class) {

                        if (!((List<?>) obj).isEmpty()
                            && ((List<?>) obj).get(0).getClass().getPackage().equals(origem.getClass().getPackage())) {

                            Object objDest = getFromField(destino, atributoDestino);

                            obj = copyList(
                                (Collection<?>) obj,
                                (Collection<T>) objDest,
                                obterClassDestino(destino.getClass(), ((List<?>) obj).get(0).getClass().getSimpleName()));
                        }
                    } else if (atributoOrigem.getType() == Set.class && atributoDestino.getType() == Set.class) {

                        if (!((Set<?>) obj).isEmpty()) {

                            for (Object element : ((Set<?>) obj)) {

                                if (element.getClass().getPackage().equals(origem.getClass().getPackage())) {

                                    Object objDest = getFromField(destino, atributoDestino);

                                    obj = copyList((Collection<?>) obj, (Collection<T>) objDest,
                                        obterClassDestino(destino.getClass(), element.getClass().getSimpleName()));
                                }

                                break;
                            }
                        }
                    } else if (atributoOrigem.getType().getPackage().equals(origem.getClass().getPackage())) {

                        obj = copyBean((T) obj, (V) atributoDestino.getType().newInstance());
                    }

                } catch (LazyInitializationException e) {

                    LOGGER.warn(e.getMessage());
                    continue;
                }

                // --------------------------------------------------------------------------------------
                setToField(destino, atributoDestino, obj);
            }

        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }

        return destino;
    }

    /**
     * Cópia os valores dos atributos de cada objeto da coleção de origem para cada objeto da coleção de destino
     * (<tt>quando o abributo no objeto de origem tiver o mesmo nome de um atributo no objeto de destino</tt>).
     * 
     * @param colecaoOrigem {@link Collection} coleção de origem.
     * @param colecaoDestino {@link Collection} coleção de destino.
     * @param tipoColecaoDestino {@link Class} tipo da coleção de destino.
     * @return {@link Collection} coleção de destino preenchida.
     * @throws IllegalArgumentException
     */
    @SuppressWarnings("unchecked")
    public static <T> Collection<T> copyList(Collection<?> colecaoOrigem, Collection<T> colecaoDestino,
        Class<?> tipoColecaoDestino) {

        if (colecaoOrigem == null) {
            return null;
        }

        try {

            T objetoDestino = null;

            for (Object objetoOrigem : colecaoOrigem) {
                objetoDestino = (T) tipoColecaoDestino.newInstance();
                colecaoDestino.add(copyBean(objetoOrigem, objetoDestino));
            }

        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }

        return colecaoDestino;
    }

    /**
     * Copia os valores dos atributos do objeto de origem para o objeto de destino (<tt>quando o abributo no objeto de
     * origem tiver o mesmo nome de um atributo no objeto de destino</tt>).
     * 
     * @param objetoDestino Objeto de destino.
     * @param objetoOrigem Objeto de origem.
     * @throws Exception
     */
    public static void copiarPropriedades(Object objetoDestino, Object objetoOrigem) {

        try {
            BeanUtils.copyProperties(objetoDestino, objetoOrigem);
        } catch (Exception e) {
            throw new ApplicationException(e);
        }
    }

    /**
     * Retorna uma nova instância da classe informada.
     * 
     * @param classe Classe que terá uma nova instância.
     * @return Nova instância da classe informada.
     * @throws Exception
     */
    // FIXME arrumar esse throws Exception
    public static <T> T obterInstancia(Class<T> classe) throws Exception {

        return classe.newInstance();
    }

    /**
     * Retorna uma nova instância da classe informada.
     * 
     * @param classe Classe que terá uma nova instância.
     * @param tipos Tipos dos parâmetros que serão passados ao contrutor.
     * @param objeto Objetos que serão passados ao construtor.
     * @return Nova instância da classe informada.
     * @throws Exception
     */
    public static <T> T obterInstancia(Class<T> classe, Class<?> tipos, Object... objeto) throws Exception {

        return classe.getConstructor(tipos).newInstance(objeto);
    }

    /**
     * Executa o método GET para o atributo informado.
     * 
     * @param classe do objeto que terá o valor do atributo definido.
     * @param nomeAtributo Nome do atributo.
     * @param objeto Objeto que terá o valor do atributo definido.
     * @return Valor do atributo definido no objeto.
     * @throws Exception
     */
    public static Object executarMetodoGet(Class<? extends Object> classe, String nomeAtributo, Object objeto)
        throws Exception {

        Object obj = null;

        String nomeMetodo = null;

        Object[] parametros = {};

        if (nomeAtributo != null) {

            if (nomeAtributo.contains(".")) {

                int indicePrimeiroPonto = nomeAtributo.indexOf(".");
                String primeiroAtributo = nomeAtributo.substring(0, indicePrimeiroPonto);
                String demaisAtributos = nomeAtributo.substring(indicePrimeiroPonto + 1);

                nomeMetodo = obterNomeMetodoGetSet(primeiroAtributo, TipoMetodo.GET);

                obj = executarMetodo(classe, nomeMetodo, objeto, parametros);

                obj = executarMetodoGet(obj.getClass(), demaisAtributos, obj);

            } else {

                nomeMetodo = obterNomeMetodoGetSet(nomeAtributo, TipoMetodo.GET);

                obj = executarMetodo(classe, nomeMetodo, objeto, parametros);
            }
        }
        return obj;
    }

    /**
     * Executa o método SET para o atributo informado.
     * 
     * @param classe Classe do objeto que terá o valor do atributo definido.
     * @param nomeAtributo Nome do atributo.
     * @param objeto Objeto que terá o valor do atributo definido.
     * @param parametros Valor que será definido no atributo.
     * @throws Exception
     */
    public static void executarMetodoSet(Class<? extends Object> classe, String nomeAtributo, Object objeto,
        Object... parametros) throws Exception {

        Object obj = null;

        String nomeMetodo = null;

        if (nomeAtributo != null) {

            if (nomeAtributo.contains(".")) {

                Object[] parametrosGet = {};

                int indicePrimeiroPonto = nomeAtributo.indexOf(".");
                String primeiroAtributo = nomeAtributo.substring(0, indicePrimeiroPonto);
                String demaisAtributos = nomeAtributo.substring(indicePrimeiroPonto + 1);

                nomeMetodo = obterNomeMetodoGetSet(primeiroAtributo, TipoMetodo.GET);

                obj = executarMetodo(classe, nomeMetodo, objeto, parametrosGet);

                executarMetodoSet(obj.getClass(), demaisAtributos, obj, parametros);

            } else {

                nomeMetodo = obterNomeMetodoGetSet(nomeAtributo, TipoMetodo.SET);

                executarMetodo(classe, nomeMetodo, objeto, parametros);
            }
        }
    }

    /**
     * Executa um método através de reflexão.
     * 
     * @param classe Classe do objeto que possui o método que será executado.
     * @param nomeMetodo Nome do método que será executado.
     * @param objeto Objeto que possui o método que será executado.
     * @param parametros Parâmetros (<tt>caso existam</tt>) que seráo passados ao método.
     * @return Valor de retorno do método.
     * @throws Exception Exceção Lançada.
     */
    public static Object executarMetodo(Class<? extends Object> classe, String nomeMetodo, Object objeto,
        Object... parametros) throws Exception {

        Object retorno = null;

        Method metodo = obterMetodoPorNome(classe, nomeMetodo);

        if (metodo != null) {

            metodo.setAccessible(true);
            retorno = executarMetodo(metodo, objeto, parametros);
        }
        return retorno;
    }

    /**
     * Executa um método através de reflexão.
     * 
     * @param metodo {@link Method} representando o método que será invocado.
     * @param objeto Objeto que possui o método que será executado.
     * @param parametros Parâmetros (<tt>caso existam</tt>) que seráo passados ao método.
     * @return Valor de retorno do método.
     * @throws Exception Exceção Lançada.
     */
    public static Object executarMetodo(Method metodo, Object objeto, Object... parametros) throws Exception {

        return metodo.invoke(objeto, parametros);
    }

    /**
     * Verifica se o objeto possui algum atributo anotado com uma determinada {@link Annotation} ou {@link Qualifier}.
     * 
     * @param classeAnotacao Classe da anotação ou qualificador.
     * @param classeObjeto Classe do objeto que será verificado.
     * @param objeto Objeto que será verificado.
     * @return <tt>TRUE</tt> se o objeto possuir algum atributo anotado com a {@link Annotation} informada,
     *         <tt>FALSE</tt> caso contrário.
     */
    public static boolean existeAnotacao(Class<? extends Annotation> classeAnotacao, Class<?> classeObjeto,
        Object objeto) {

        for (Field field : classeObjeto.getDeclaredFields()) {

            field.setAccessible(true);

            if ((field.getAnnotation(classeAnotacao)) != null) {
                return true;
            }
        }
        return false;
    }

    /**
     * Retorna o valor do atributo mapeado com a {@link Annotation} {@link Id}.
     * 
     * @param classeObjeto Classe do objeto que será verificado.
     * @param objeto Objeto que será verificado.
     * @return Valor do atributo mapeado com {@link Id}.
     * @throws Exception
     */
    public static Object obterValorCampoId(Class<?> classeObjeto, Object objeto) throws Exception {

        Object valor = null;

        List<Field> fieds = obterFieldsAnotados(Id.class, classeObjeto, objeto);

        if (!fieds.isEmpty()) {

            Field field = fieds.get(0);
            valor = field.getType().cast(field.get(objeto));
        }

        return valor;
    }

    /**
     * Especifica o valor do atributo mapeado com a {@link Annotation} {@link Id}.
     * 
     * @param classeObjeto Classe do objeto que será verificado.
     * @param objeto Objeto que será verificado.
     * @param valor Valor que será atribuído ao atributo.
     * @return Valor do atributo mapeado com {@link Id}.
     * @throws Exception
     */
    public static void setValorCampoId(Class<?> classeObjeto, Object objeto, Object valor) throws Exception {

        List<Field> fieds = obterFieldsAnotados(Id.class, classeObjeto, objeto);

        if (!fieds.isEmpty()) {

            Field field = fieds.get(0);
            field.set(objeto, valor);
        }
    }

    /**
     * Obtém um {@link Map} com os nomes e seus respectivos valores dos atributos anotados com uma determinada
     * {@link Annotation} ou {@link Qualifier}.
     * 
     * @param classeAnotacao Classe da anotação ou qualificador.
     * @param classeObjeto Classe do objeto que será verificado.
     * @param objeto Objeto que será verificado.
     * @return {@link MappingException} dos atrubutos anotados.
     * @throws Exception
     */
    public static Map<String, Object> obterCamposAnotados(Class<? extends Annotation> classeAnotacao,
        Class<?> classeObjeto, Object objeto) throws Exception {

        Map<String, Object> camposAnotados = new HashMap<String, Object>();
        List<Field> fields = obterFieldsAnotados(classeAnotacao, classeObjeto, objeto);

        for (Field field : fields) {

            camposAnotados.put(field.getName(), field.get(classeObjeto.cast(objeto)));
        }

        return camposAnotados;
    }

    /**
     * Remove os espaços em branco desnecessários dos atributos do tipo {@link String} do objeto passado.
     * 
     * @param entity Entidade mapeada com JPA.
     * @throws Exception
     */
    public static <T extends BaseEntity> void removerEspacos(T entity) throws Exception {

        Class<?> classeObjeto = entity.getClass();

        for (Field field : classeObjeto.getDeclaredFields()) {

            field.setAccessible(true);

            if (field.get(entity) instanceof String) {

                field.set(entity, StringUtil.removerEspacos((String) field.get(entity)));
            } else if (field.get(entity) instanceof BaseEntity) {

                BaseEntity fieldEntity = (BaseEntity) field.get(entity);
                removerEspacos(fieldEntity);
            }
        }
    }

    /**
     * Obtém uma lista com os atributos anotados com uma determinada {@link Annotation} ou {@link Qualifier}.
     * 
     * @param classeAnotacao Classe da anotação ou qualificador.
     * @param classeObjeto Classe do objeto que será verificado.
     * @param objeto Objeto que será verificado.
     * @return {@link Field} caso exista um atributo com a anotação informada, <tt>null</tt> caso contrário.
     * @throws Exception
     */
    private static List<Field> obterFieldsAnotados(Class<? extends Annotation> classeAnotacao, Class<?> classeObjeto,
        Object objeto) throws Exception {

        List<Field> fields = new ArrayList<Field>();

        for (Field field : classeObjeto.getDeclaredFields()) {
            if ((field.getAnnotation(classeAnotacao)) != null) {
                Field privateField = classeObjeto.getDeclaredField(field.getName());
                privateField.setAccessible(true);

                fields.add(privateField);
            }
        }
        return fields;
    }

    /**
     * Retorna a classe do objeto de destino.
     * 
     * @param destino {@link Class} Tipo do objeto de destino.
     * @param nomeClasseObjetoOrigem Nome da classe do objeto de origem.
     * @return {@link Class} Tipo do objeto de destino.
     * @throws ClassNotFoundException
     */
    @SuppressWarnings("unchecked")
    private static <T> Class<T> obterClassDestino(Class<T> destino, String nomeClasseObjetoOrigem)
        throws ClassNotFoundException {

        StringBuilder sb = new StringBuilder().append(destino.getPackage().getName()).append(".")
            .append(nomeClasseObjetoOrigem.substring(0, nomeClasseObjetoOrigem.length() - 2))
            .append(nomeClasseObjetoOrigem.endsWith(TO) ? PO : TO);

        return (Class<T>) Class.forName(sb.toString());
    }

    /**
     * Retorna o mapa com os atributos (<tt>nome do atributo como chave e o próprio atributo como valor</tt>).
     * 
     * @param fields Array de {@link Field} Atributos.
     * @return Mapa de atributos.
     */
    private static Map<String, Field> gerarMap(Field[] fields) {

        Map<String, Field> retorno = new HashMap<String, Field>();

        for (Field field : fields) {
            retorno.put(field.getName(), field);
        }

        return retorno;
    }

    /**
     * Invoca o método <tt>get</tt> do atributo no objeto informado.
     * 
     * @param objeto Objeto que possui o atributo.
     * @param atributo {@link Field} Atributo do objeto.
     * @return Valor do atributo.
     * @throws Exception
     */
    private static Object getFromField(Object objeto, Field atributo) throws Exception {

        Method metodoGet = obterMetodo(objeto.getClass(), GET, atributo);

        return executarMetodo(metodoGet, objeto);
    }

    /**
     * Invoca o método <tt>set</tt> do atributo no objeto informado.
     * 
     * @param objeto Objeto que possui o atributo.
     * @param atributo {@link Field} Atributo do objeto.
     * @param parametros Parâmetros do método <tt>set</tt> do atributo.
     * @throws Exception
     */
    private static void setToField(Object objeto, Field atributo, Object parametros) throws Exception {

        Method metodoSet = obterMetodo(objeto.getClass(), SET, atributo);

        executarMetodo(metodoSet, objeto, parametros);
    }

    /**
     * Retorna o método <tt>get</tt> ou <tt>set</tt> do atributo informado.
     * 
     * @param tipoObjeto {@link Class} Tipo do objeto que possui o atributo.
     * @param tipoMetodo Tipo do método (<tt>get</tt> ou <tt>set</tt>).
     * @param atributo {@link Field} Atributo do objeto.
     * @return {@link Method} método.
     * @throws Exception
     */
    private static Method obterMetodo(Class<?> tipoObjeto, String tipoMetodo, Field atributo) throws Exception {

        Class<?>[] tiposParametros = null;

        if (SET.equals(tipoMetodo)) {

            tiposParametros = new Class[] { atributo.getType() };
        }

        Method metodo = null;

        StringBuilder sb = new StringBuilder(tipoMetodo);

        try {

            metodo = tipoObjeto.getMethod(sb.append(obterFieldName(atributo)).toString(), tiposParametros);

        } catch (NoSuchMethodException e) {

            metodo = tipoObjeto
                .getMethod(sb.insert(0, IS).append(obterFieldName(atributo)).toString(), tiposParametros);
        }

        return metodo;
    }

    /**
     * Retorna o nome do atributo.
     * 
     * @param atributo {@link Field} Atributo do objeto.
     * @return Nome do atributo.
     */
    private static String obterFieldName(Field atributo) {

        return Character.toUpperCase(atributo.getName().charAt(0)) + atributo.getName().substring(1);
    }

    /**
     * Retorna o {@link Method} pelo nome na classe informada. Caso não encontre, procura recursivamente pelo
     * {@link Method} em sua superclasse até encontrar o método ou até que a superclasse seja {@link Object}.
     * 
     * @param classe Classe do objeto que possui o método que será executado.
     * @param nomeMetodo Nome do método.
     * @return {@link Method}.
     */
    private static Method obterMetodoPorNome(Class<? extends Object> classe, String nomeMetodo) {

        Method metodo = null;

        if (classe != null && !classe.getSimpleName().equals(Object.class.getSimpleName())) {

            for (Method method : classe.getDeclaredMethods()) {

                if (method.getName().equals(nomeMetodo)) {
                    metodo = method;
                    break;
                }
            }

            if (metodo == null) {

                metodo = obterMetodoPorNome(classe.getSuperclass(), nomeMetodo);
            }
        }

        return metodo;
    }

    /**
     * Retorna o nome do método GET/SET para o atributo informado.
     * 
     * @param nomeAtributo Nome do atributo.
     * @param tipoMetodo {@link TipoMetodo} Tipo do método (<tt>GET</tt> ou <tt>SET</tt>).
     * @return Nome do método.
     */
    private static String obterNomeMetodoGetSet(String nomeAtributo, TipoMetodo tipoMetodo) {

        StringBuilder sb = new StringBuilder();
        sb.append(tipoMetodo.getTipoMetodo());
        sb.append(nomeAtributo.substring(0, 1).toUpperCase());
        sb.append(nomeAtributo.substring(1));
        return sb.toString();
    }

    /**
     * Enumeração com os tipos de métodos que podem ser executados no método <b>obterNomeMetodoGetSet(String,
     * String)</b>.
     * 
     * @author Vitor Franco do Carmo (vitor.carmo@capgemini.com)
     * @since 03/04/2013
     * 
     */
    protected enum TipoMetodo {
        SET("set"),
        GET("get");

        private String tipoMetodo;

        private TipoMetodo(String tipoMetodo) {

            this.tipoMetodo = tipoMetodo;
        }

        /**
         * Retorna o valor do atributo tipoMetodo.
         * 
         * @return Valor do atributo tipoMetodo.
         */
        public String getTipoMetodo() {

            return tipoMetodo;
        }

    }

}
